package ar.com.arkios.kfcon4d.dao;

import ar.com.arkios.kfcon4d.Connection4D;
import ar.com.arkios.kfcon4d.exception.Open4DException;
import ar.com.arkios.kfcon4d.util.CuatroDUtils;
import ar.com.arkios.kfcon4d.util.EstructuraUtil;
import ar.com.arkios.j4d.opConstants;
import ar.com.arkios.j4d.opData;
import ar.com.arkios.j4d.opDataArray;
import ar.com.arkios.j4d.opException;
import ar.com.arkios.j4d.opFieldArray;
import ar.com.arkios.j4d.opProcess;
import ar.com.arkios.j4d.opSelection;
import ar.com.arkios.j4d.opTable;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.collections.CollectionUtils;
import org.apache.log4j.Logger;

/**
 * <p>
 * Prevee todos los métodos comunes para cualquier DAO de la aplicación.
 * </p>
 * 
 * <p>
 * Inicializa la conección y el proceso a ser utilizados por el DAO para 
 * comunicarse con la base de datos. Por lo tanto, no es necesario crear 
 * conexión o proceso nuevo. Los mismo ya existen y son inicializados por esta
 * entidad.
 * </p>
 * 
 * <p>
 * <b>Todo DAO de la aplicación debería extender de esta clase</b> para hacer 
 * uso de las funcionalidades comunes que provee la misma.
 * </p>
 * 
 * @author hcarrizo
 */
public abstract class GenericDAO<T extends Comparable> {
    
    private static final Logger miLogger = Logger.getLogger(GenericDAO.class);
    
    private String miNombre;
    /** Conección a 4D */
    protected Connection4D miConexion;
    
    /** 
     * Proceso a través del cual se ejecutan las consultas a la base de datos 
     * de 4D.
     */
    protected opProcess miProceso;
    
    private opTable miTablaEntidad;
    private opTable miTablaEntidadAtributos;
    
    /**
     * Indica si se debe o no reescribir el registro en caso de realizar un save
     * y el mismo ya exista en la base.
     */
    private boolean miReescritura = false;
    
    /**
     * Cantidad de registros a modificar o dar de alta en cada mini transaccion de 4D
     * Para no colgar totalmente el servidor
     */
    private int miCantRegsPorBloque = 150;
    
    /**
     * Cantidad de milisegundos a dormir entre cada mini transaccion, para que el
     * server 4D respire y le de bola a los demás clientes.
     */
    private int misMillisegADomir = 450;

    private opFieldArray miEntidadFields;
    private opFieldArray miEntidadPKFields;
    private opFieldArray miEntidadAtributosFields;
    private opFieldArray miEntidadAtributosPKFields;  
    
    public GenericDAO() {
        miConexion = Connection4D.getInstance();
        miProceso = miConexion.getMyProcess();
        miTablaEntidad = miConexion.getTable(getEntidadTableName());
        miEntidadFields = EstructuraUtil.getFieldArrayFor(miTablaEntidad, getEntidadFieldsNames());
        miEntidadPKFields = EstructuraUtil.getFieldArrayFor(miTablaEntidad, getEntidadPKFieldsNames());
        
        if (getEntidadAtributoTableName() != null){
            miTablaEntidadAtributos = miConexion.getTable(getEntidadAtributoTableName());
            miEntidadAtributosFields = EstructuraUtil.getFieldArrayFor(miTablaEntidadAtributos, getEntidadAtributosFieldsNames());
            miEntidadAtributosPKFields = EstructuraUtil.getFieldArrayFor(miTablaEntidadAtributos, getEntidadAtributosPKFieldsNames());
        } else {
            miTablaEntidadAtributos = null;
            miEntidadAtributosFields = null;
            miEntidadAtributosPKFields = null;            
        }
        this.miNombre = this.getClass().getName();
        this.miNombre = this.miNombre.substring(this.miNombre.lastIndexOf(".")+1);
    }
    /*
     * El codigo de este metodo esta tomando un par de consideraciones (hardcode)
     * para facilitar el desarrollo en su principio. Las consideraciones son:
     *  - Los daos estan asociados a una tabla principal. Si despues se mapea a
     * dos o mas tablas es otro tema, los repetidos o no los saca con la clave de
     * la tabla principal.
     *  - El codigo supone que la tabla tiene una clave principal de UNA columna,
     * es decir, no soporta claves compuestas.
     */
    public void saveAll(List<T> listaModelo) throws opException {
        try {
            // Obtengo la lista de códigos de los registros existentes en la base.
            miLogger.debug("Inicio saveAll "+miNombre+" .Lista recibida con "+listaModelo.size()+" elementos");
            Set<String> claves = getListaCodigos();
            Set<String> clavesModificar = new HashSet<String>();

            // Divido la lista modelo en dos, los de ALTA y los de MODIFICACION.
            ArrayList<T> listaAlta = new ArrayList<T>();
            ArrayList<T> listaModificacion = new ArrayList<T>();

            // Separo la lista del modelo a persistir en dos: los que se tienen
            // que crear y los que se tienen que modificar.
            for (T elem : listaModelo) {
                String value = getEntidadKeyValue(elem);
                
                if (claves.contains(value)) {
                    listaModificacion.add(elem);
                    clavesModificar.add(value);
                } else {
                    listaAlta.add(elem);
                }
            }
            
            Collection<String> clavesViejas = CollectionUtils.subtract(claves, clavesModificar);
            miLogger.debug("SaveAll "+miNombre+": listas preparadas. Mod: "+listaModificacion.size()+" - Alta: "+listaAlta.size()+" - Baja: "+clavesViejas.size());
            if (miReescritura && (listaModificacion.size() > 0)) {
                Collections.sort(listaModificacion);
                opDataArray[] lista4DModificacion = mapListEntidadTo4D(listaModificacion);
                opDataArray[] lista4DAtributosModificacion = null;
                if (this.hasAtributos())
                    lista4DAtributosModificacion = mapListEntidadAtributosTo4D(listaModificacion);
                opDataArray[] lista4DModificacionPK = null;
                opSelection seleccion = null;
                opSelection seleccionAtributos = null;
                miProceso.StartTransaction();
                miLogger.debug("reescritura: listas armadas. Cantidad a modificar: "+listaModificacion.size());
                if (listaModificacion.size()  < clavesViejas.size()) { // ------- BUSQUEDA "AL DERECHO" ---------
                    lista4DModificacionPK = convert4DListEntidadToPK(lista4DModificacion);
                    seleccion = CuatroDUtils.buscarXlista(miProceso, lista4DModificacionPK, miEntidadPKFields);
                    if (this.hasAtributos())
                        seleccionAtributos = CuatroDUtils.buscarXAtributoslista(miProceso, lista4DModificacionPK, miEntidadAtributosPKFields);
                    miLogger.debug("Búsqueda directa terminada. Encontrados: "+seleccion.mRecordsInSelection);
                } else {// -------------------   BUSQUEDA "INVERSA" -----------------------
                    // armo busqueda complementaria e invierto seleccion
                    if (clavesViejas.size() > 0) {
                        opDataArray pkViejas = new opDataArray(clavesViejas.size());
                        int i = 0;
                        for (String clave : clavesViejas) {
                            pkViejas.mDataArray[i] = new opData(opConstants.ALPHANUMERIC, clave);
                            i++;
                        }
                        seleccion = CuatroDUtils.buscarXlista(miProceso, pkViejas, miEntidadPKFields);
                        // luego llamar a CuatroDUtils.invertirSeleccion
                        seleccion = CuatroDUtils.invertirSeleccion(miProceso, miEntidadPKFields.mTargetTable);
                        if (this.hasAtributos()) {
                            seleccionAtributos = CuatroDUtils.buscarXlista(miProceso, pkViejas, miEntidadAtributosPKFields);
                            seleccionAtributos = CuatroDUtils.invertirSeleccion(miProceso, miEntidadAtributosPKFields.mTargetTable);
                            seleccionAtributos = CuatroDUtils.ySAP(miProceso, miEntidadAtributosFields.mTargetTable);
                        }  
                    } else {
                        seleccion = miProceso.AllRecords(miTablaEntidad);
                        if (this.hasAtributos()) {
                            seleccionAtributos = miProceso.AllRecords(miTablaEntidadAtributos);
                            seleccionAtributos = CuatroDUtils.ySAP(miProceso, miEntidadAtributosFields.mTargetTable);
                        }
                    }
                    miLogger.debug("Búsqueda inversa terminada. Econtrados: "+seleccion.mRecordsInSelection);
                }//--------------------------------- FIN BUSQUEDA ----------------------
                // ahora en 4D esta la seleccion de registros a modificar
                boolean todoOk = (seleccion.mRecordsInSelection == listaModificacion.size());
                if (this.hasAtributos())
                    todoOk = todoOk && (seleccionAtributos.mRecordsInSelection == listaModificacion.size());
                // Ordeno la seleccion ascendentemente y hago arrayToSeleccion
                if (todoOk) {
                    CuatroDUtils.ordenar(miProceso, miEntidadPKFields, opConstants.GREATER_THAN);
                    miProceso.ArrayToSelection(lista4DModificacion, miEntidadFields);
                    if (this.hasAtributos()) {
                        CuatroDUtils.ordenar(miProceso, miEntidadAtributosPKFields, opConstants.GREATER_THAN);
                        // me falta mapear los atributos lista4DModificacion ESTA MAL******************************
                        miProceso.ArrayToSelection(lista4DAtributosModificacion, miEntidadAtributosFields);
                    }
                    miProceso.ValidateTransaction();
                } else {
                    miProceso.CancelTransaction();
                    miLogger.error("Cantidades encontradas y buscada no coincidentes.");
                    miLogger.error("Buscados: "+listaModificacion.size());
                    miLogger.error("Encontrados: "+seleccion.mRecordsInSelection);
                    if(this.hasAtributos())
                        miLogger.error("Atributos Encontrados: "+seleccionAtributos.mRecordsInSelection);
                }
            }
            // Impacto las altas de los registros en ALTA.
            if (listaAlta.size() > 0) {
                miLogger.debug("Inicios alta de "+listaAlta.size()+" registros de "+miNombre);
                //----------------------------------------------------------------------------------------------------
                int aProcesar;
                int desde = 0;
                int hasta;
                int faltan = listaAlta.size();
//                long timeI;
//                long timeF;
                while (faltan > 0) {
                    aProcesar = Math.min(faltan, miCantRegsPorBloque);
                    hasta = desde + aProcesar;
                    miLogger.debug(miNombre+" - desde: "+desde+" - hasta: "+hasta);
//                    timeI = System.currentTimeMillis();
                    miProceso.StartTransaction();
//                    timeF = System.currentTimeMillis();
//                    miLogger.debug("transacción iniciada: "+ (timeF-timeI));
//                    timeI = System.currentTimeMillis();
                    opDataArray[] lista4DAlta = mapListEntidadTo4D(listaAlta.subList(desde, hasta));
                    CuatroDUtils.alta(miProceso, lista4DAlta, miEntidadFields);
                    if (this.hasAtributos()) {
                        opDataArray[] lista4DAtributosAlta = mapListEntidadAtributosTo4D(listaAlta.subList(desde, hasta));
                        CuatroDUtils.alta(miProceso, lista4DAtributosAlta, miEntidadAtributosFields);
                    } 
//                    timeF = System.currentTimeMillis();
//                    miLogger.debug("trabajo hecho: "+ (timeF-timeI));
//                    timeI = System.currentTimeMillis();
                    miProceso.ValidateTransaction();
//                    timeF = System.currentTimeMillis();
//                    miLogger.debug("transacción validada: "+ (timeF-timeI));
                    Thread.sleep(misMillisegADomir);
                    
                    desde += aProcesar;
                    faltan -= aProcesar;
                }
                //----------------------------------------------------------------------------------------------------
                miLogger.debug("Alta finalizada");                
            }
            //miProceso.ValidateTransaction();
        } catch (opException ex) {
            miLogger.error("Error durante la actualización de "+this.miNombre);
            miLogger.error(ex.GetErrorMsg());
            miProceso.CancelTransaction();
        } catch (Exception e) {
            miLogger.error("Ocurrió un error inesperado en la operación.");
            miLogger.error(e);
            miProceso.CancelTransaction();
        }
    }

    protected String getEntidadKeyValue(T elem) {
        try {
            return BeanUtils.getProperty(elem, getPKProperty());
        } catch (InvocationTargetException e) {
            miLogger.error("No se pudo invocar el método para obtener el valor de la propiedad " + getPKProperty() + " de la entidad " + elem.getClass().getName());
            throw new Open4DException(e);
        } catch (IllegalAccessException e) {
            miLogger.error("No se pudo acceder al método para la obtención de la propiedad " + getPKProperty());
            throw new Open4DException(e);
        } catch (NoSuchMethodException e) {
            miLogger.error("No se encontró el método para acceder a la propiedad " + getPKProperty() + ". Verifique que existe el método get y set de la propiedad indicada.");
            throw new Open4DException(e);
        }
    }

    // TODO: hacer esto de manera generica, para que los códigos puedan ser de
    // cualquier tipo. QUEDA PENDIENTE SI TIENE PK COMPUESTA COMO DEVUELVO EL
    // EL ELEMENTO DE LA COLECCIÓN
    private Set<String> getListaCodigos() {
        Set<String> result = new HashSet<String>();
        try {
            opSelection selection = miProceso.AllRecords(miTablaEntidad);
            opDataArray[] dataArrayPK = new opDataArray[miEntidadPKFields.mSize];//cambiado para claves compuestas
            for (int i = 0; i < dataArrayPK.length; i++) {
                dataArrayPK[i] = new opDataArray(selection.mRecordsInSelection);
            }
            
            // TODO: cambiar cuando se empeice  a trabajar con claves compuestas.
            //miLogger.debug("getListaCodigos a punto de hacer selection to array. Regs en seleccion: " + selection.mRecordsInSelection);
            miProceso.SelectionToArray(dataArrayPK, miEntidadPKFields);
            //miLogger.debug("getListaCodigos pasó selection to array. DataArray de " + dataArrayPK[0].mSize);
            // Por ahora hago un for hasta la cantidad de registros. Cuando tenga
            // clave compuesta creo que voy a tener un FOR mas externo, para las claves.
            // y concatenar claves con separador especial ????????????
            for (int i = 0; i < dataArrayPK[0].mSize; i++) {
                String clave = dataArrayPK[0].mDataArray[i].mString;
                result.add(clave);
            }
                       
        } catch (opException e) {
            miLogger.error("No se pudo acceder a los codigos de "+miNombre);
            miLogger.error(e);
            throw new Open4DException(e);
        } catch (Exception e) {
            miLogger.error("Ocurrió un error inesperado en getListaCodigos");
            miLogger.error(e);
            throw e;
        } finally {
            return result;
        }
    }
    
    private boolean hasAtributos() {
        return miTablaEntidadAtributos != null;
    }
    protected abstract String getEntidadTableName();
    
    protected String getEntidadAtributoTableName(){
        // No hace nada excepto que lo sobreescriban.
        return null;
    }
    
    protected abstract String getPKProperty();
    
    /**
     * Data un lista de entidades la convierte a modo lista para 4D
     * 
     * @param laLista lista de entidades a convertir
     * @return lista 4D equivalente
     */
    protected abstract opDataArray[] mapListEntidadTo4D (List<T> laLista);
    
    /**
     * Data una lista de entidades en formato 4D arma una lista equivalente tambien en formato 4D
     * pero con solamente las PK
     * 
     * @param laLista con las entidaes
     * @return lista de solo las PK
     */
    protected opDataArray[] convert4DListEntidadToPK (opDataArray[] laLista) {
        opDataArray[] lasPK = new opDataArray[getEntidadPKFieldsNames().length];
        for (int i = 0; i < getEntidadPKFieldsNames().length; i++) {
            int j;
            for (j = 0; j < getEntidadFieldsNames().length; j++) {
                if (getEntidadPKFieldsNames()[i].equals(getEntidadFieldsNames()[j]))
                    break;
            }
            if (j < getEntidadFieldsNames().length) {
                lasPK[i] = laLista[j];
            } else {
                miLogger.error("No se encontro la columna PK '" + getEntidadPKFieldsNames()[i] + "' en las columnas indicadas en el DAO.");
                throw new Open4DException("Error en el mapeo de columnas y columnas PK del DAO");
            }
        }
        return lasPK;
    }
    
    protected opDataArray[] mapListEntidadAtributosTo4D (List<T> laLista) {
        return new opDataArray[0];
    }

    public boolean isReescritura() {
        return miReescritura;
    }

    public void setReescritura(boolean reescritura) {
        this.miReescritura = reescritura;
    }
        
    /**
     * Devuelve los nombres de las columnas de la entidad a ser tenidos en cuenta.
     */
    protected abstract String[] getEntidadFieldsNames();

    /**
     * Devuelve los nombres de las columnas de la entidad a ser tenidos en cuenta.
     */
    protected String[] getEntidadAtributosFieldsNames() {
        // No hace nada excepto que lo sobreescriban.
        String[] nada = {};
        return nada;
    }
    
    /**
     * Devuelve los nombres de las columnas de la PK de la entidad.
     */
    protected abstract String[] getEntidadPKFieldsNames();

    /**
     * Devuelve los nombres de las columnas de la PK de los atributos de la entidad.
     */
    protected String[] getEntidadAtributosPKFieldsNames() {
        // No hace nada excepto que lo sobreescriban.
        String[] nada = {};
        return nada;
    }
   
    protected opFieldArray getMiEntidadAtributosFields() {
        return miEntidadAtributosFields;
}

    protected opFieldArray getMiEntidadAtributosPKFields() {
        return miEntidadAtributosPKFields;
    }

    protected opFieldArray getMiEntidadFields() {
        return miEntidadFields;
    }

    protected opFieldArray getMiEntidadPKFields() {
        return miEntidadPKFields;
    }

    protected opTable getMiTablaEntidad() {
        return miTablaEntidad;
    }

    protected opTable getMiTablaEntidadAtributos() {
        return miTablaEntidadAtributos;
    }

    public int getCantRegsPorBloque() {
        return miCantRegsPorBloque;
    }

    public void setCantRegsPorBloque(int cantRegsPorBloque) {
        this.miCantRegsPorBloque = cantRegsPorBloque;
    }

    public int getMillisegADomir() {
        return misMillisegADomir;
    }

    public void setMillisegADomir(int millisegADomir) {
        this.misMillisegADomir = millisegADomir;
    }
    
}
